/* bzflag
 * Copyright (c) 1993-2020 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

// interface header
#include "OSFile.h"

// system headers
#include <vector>
#include <string> //std::string
#include <algorithm>
#include <string.h> //c-style string
#include <stdio.h>
#ifndef _MSC_VER
#include <stddef.h>
#endif

// local implementation headers
#include "common.h"
#include "TextUtils.h"

typedef std::vector<std::string> fileNameList;

// These are the core file name conversions
// std pathnames are unix style
// If you are adding a platform
// this is one place you need to add conversions to/from your OS

// utility functions
void OSFileStdToOSDir(std::string &dir)
{
    if (dir=="") return;
#ifndef _WIN32
    return;
#else
    std::replace(dir.begin(), dir.end(), '/', '\\');
#endif//WIN32
}

void OSFileOSToStdDir(std::string &dir)
{
//#ifndef _WIN32
//  return;
//#else
    std::replace(dir.begin(), dir.end(), '\\', '/');
//#endif//WIN32
}

// Windows specific make path
#ifdef _WIN32
void windowsMakePath(const std::string &path)
{
    std::string::size_type pos = 0;
    std::string::size_type delimitpos;

    do
    {
        delimitpos = path.find('\\', pos);
        std::string nextdir = path.substr(pos, delimitpos);
        mkdir(nextdir.c_str());
        pos = delimitpos + 1;
    }
    while (delimitpos != std::string::npos);
}
#endif // windows


/// global make path
void osMakePath(const std::string &path)
{
    if (path=="") return;
#ifdef _WIN32
    windowsMakePath(path);
#endif
}


// global path stuff
std::string  stdBaseDir;
std::string  osBaseDir;

void setOSFileBaseDir(const std::string &dir)
{
    stdBaseDir = dir;
    osBaseDir = dir;
    OSFileStdToOSDir(osBaseDir);
}

// OSFileClass
class OSFile::OSFileInfo
{
public:
    OSFileInfo();
    OSFileInfo(const OSFile::OSFileInfo &r);
    std::string    stdName;
    std::string    osName;
    std::string    title;
    std::string    osPath;
    bool      useGlobalPath;
    FILE      *fp;
};

OSFile::OSFileInfo::OSFileInfo()
{
    fp = nullptr;
    useGlobalPath = true;
}

OSFile::OSFileInfo::OSFileInfo(const OSFile::OSFileInfo &r)
{
    // never copy the file
    fp = nullptr;
    osName = r.osName;
    stdName = r.stdName;
    title =  r.title;
    useGlobalPath = r.useGlobalPath;
}

void OSFile::setUseGlobalPath(bool use)
{
    info->useGlobalPath = use;
}

OSFile::OSFile()
{
    info = new OSFileInfo;
}

OSFile::OSFile(const std::string &name)
{
    info = new OSFileInfo;
    stdName(name);
}

OSFile::OSFile(const char* name)
{
    info = new OSFileInfo;
    stdName(std::string(name));
}

OSFile::OSFile(const OSFile &r)
{
    info = new OSFileInfo(*r.info);
}

OSFile& OSFile::operator = (const OSFile &r)
{
    if (this == &r)
        return *this;

    if (info)
        delete (info);

    info = new OSFileInfo(*r.info);
    return *this;
}

OSFile::OSFile(const std::string &name, const char *mode)
{
    info = new OSFileInfo;
    stdName(name);
    open(name.c_str(), mode);
}

OSFile::~OSFile()
{
    close();
    delete (info);
}

bool OSFile::open(const std::string &name, const char *mode)
{
    close();
    stdName(name);
    return open(mode);
}

bool OSFile::open(const char *mode)
{
    close();

    char  modeToUse[32];

    if (!mode)
        sprintf(modeToUse, "rb");
    else
    {
        strncpy(modeToUse, mode, sizeof(modeToUse) - 1);
        modeToUse[31] = '\0';
    }

    std::string fileName;

    if (info->useGlobalPath)
        fileName = osBaseDir;
    fileName += info->osName;

    info->fp = fopen(fileName.c_str(),modeToUse);

    // we may need to make the file path to the file, if we are writing then lets get on with it.
    if (!info->fp && strchr(modeToUse,'w'))
    {
        osMakePath(fileName);
        info->fp = fopen(fileName.c_str(),modeToUse);
    }

    return isOpen();
}

bool OSFile::close()
{
    if (info->fp)
        fclose(info->fp);

    info->fp = nullptr;

    return (!isOpen());
}

int OSFile::read(void* data, int _size, int count)
{
    if (!isOpen())
        return 0;

    return (int)fread(data, _size, count, info->fp);
}

unsigned char OSFile::readChar()
{
    if (!isOpen())
        return 0;

    int c = getc(info->fp);
    if (c!=EOF)
        return c;
    else
        return 0;
}

std::string OSFile::readLine()
{
    std::string line;
    // reserve initial space for about a typical line length to speed up things
    line.reserve(20);

    char c = readChar();
    while (c != 0 && c != '\n' && c != 10)
    {
        line += c;
        c = readChar();
    }
    return line;
}

int OSFile::scanChar(unsigned char *pChar)
{
    if (!pChar || !isOpen())
        return 0;

    return fscanf(info->fp,"%c",pChar);
}

const char* OSFile::scanStr()
{
    if (!isOpen())
        return 0;

    static char temp[1024] = {0};
    if (fscanf(info->fp,"%s",temp)!=1)
        return nullptr;
    return temp;
}

int OSFile::write(const void* data, int _size)
{
    if (!isOpen())
        return 0;

    return (int)fwrite(data, _size, 1, info->fp);
}

void OSFile::flush()
{
    fflush(info->fp);
}


int OSFile::seek(teFilePos ePos, int iOffset)
{
    if (!isOpen())
        return 0;

    long iMode;
    switch (ePos)
    {
    case eFileStart:
        iMode = SEEK_SET;
        break;

    case eFileEnd:
        iMode = SEEK_END ;
        break;

    case eCurentPos:
    default:
        iMode = SEEK_CUR ;
        break;
    }

    return fseek(info->fp,iOffset,iMode);
}

unsigned int OSFile::size()
{
    if (!isOpen())
        return 0;

    unsigned int pos = ftell(info->fp);
    fseek(info->fp,0,SEEK_END);
    unsigned int len = ftell(info->fp);
    fseek(info->fp,pos, SEEK_SET);

    return len;
}

unsigned int OSFile::tell()
{
    if (!isOpen())
        return 0;
    return ftell(info->fp);
}


void OSFile::stdName(const std::string &name)
{
    info->stdName = name;
    info->osName = name;
    OSFileStdToOSDir(info->osName);
}

void OSFile::osName(const std::string &name)
{
    info->stdName = name;
    info->osName = name;
    OSFileOSToStdDir(info->stdName);
}

std::string OSFile::getFileName()
{
    std::string::size_type lastslash = info->stdName.rfind('/');
    info->title = lastslash==std::string::npos ?
                  info->stdName : info->stdName.substr(lastslash+1);

    // strip extension
    info->title.erase(info->title.rfind('.'));

    return info->title;
}

// this CAN return npos, cus the file may not have an extenstion, if it just happens to end in a '.' then well, your really weird Mr. File.
std::string OSFile::getExtension()
{
    std::string::size_type dot = info->stdName.rfind('.');
    return dot==std::string::npos ? "" : info->stdName.substr(dot+1);
}

std::string OSFile::getOSFileDir()
{
    std::string::size_type lastslash = info->stdName.rfind('/');
    info->osPath = lastslash==std::string::npos ?
                   info->stdName : info->stdName.substr(0, lastslash);
    OSFileStdToOSDir(info->osPath);

    return info->osPath;
}

std::string OSFile::getFullOSPath()
{
    std::string path = osBaseDir;
    path += info->osName;
    return path;
}

FILE* OSFile::getFile()
{
    return info->fp;
}

std::string OSFile::getStdName()
{
    return info->stdName;
}

std::string OSFile::getOSName()
{
    return info->osName;
}

bool OSFile::isOpen()
{
    return info->fp != nullptr;
}

// OS Dir classes
class OSDir::OSDirInfo
{
public:
    OSDirInfo();
    OSDirInfo(const OSDir::OSDirInfo &r);

    std::string    baseStdDir;
    std::string    baseOSDir;
    fileNameList    nameList;
    int      namePos;
};

OSDir::OSDirInfo::OSDirInfo()
{
    namePos = -1;
}

OSDir::OSDirInfo::OSDirInfo(const OSDir::OSDirInfo &r)
{
    baseStdDir = r.baseStdDir;
    baseOSDir = r.baseOSDir;
    nameList = r.nameList;
    namePos = r.namePos;
}

OSDir::OSDir()
{
    info = new OSDirInfo;
}

OSDir::OSDir(const OSDir& r)
{
    info = new OSDirInfo(*r.info);
}

OSDir& OSDir::operator = (const OSDir& r)
{
    if (this == &r)
        return *this;

    delete info;
    info = new OSDirInfo(*r.info);

    return *this;
}

OSDir::OSDir(const std::string &DirName)
{
    info = new OSDirInfo;
    setStdDir(DirName);
}

OSDir::~OSDir()
{
    if (info)
    {
        info->nameList.clear();
        delete(info);
    }
}

void OSDir::setStdDir(const std::string &DirName)
{
    info->baseStdDir = DirName;
    info->baseOSDir = DirName;
    OSFileStdToOSDir(info->baseOSDir);
}

void OSDir::setOSDir(const std::string &DirName)
{
    info->baseStdDir = DirName;
    info->baseOSDir = DirName;
    OSFileOSToStdDir(info->baseStdDir);
}

void OSDir::makeStdDir(const std::string &DirName)
{
    setStdDir(DirName);
#ifdef _WIN32
    mkdir(info->baseOSDir.c_str());
#else
    mkdir(info->baseOSDir.c_str(), 0777);
#endif
}

void OSDir::makeOSDir(const std::string &DirName)
{
    setOSDir(DirName);
#ifdef _WIN32
    mkdir(info->baseOSDir.c_str());
#else
    mkdir(info->baseOSDir.c_str(), 0777);
#endif
}

std::string OSDir::getStdName()
{
    return info->baseStdDir;
}

std::string  OSDir::getOSName()
{
    return info->baseOSDir.c_str();
}

std::string OSDir::getFullOSPath()
{
    std::string  FilePath = osBaseDir;
    FilePath += info->baseOSDir;
    return FilePath;
}

bool OSDir::getNextFile(OSFile &oFile, bool bRecursive)
{
    return getNextFile(oFile, nullptr, bRecursive);
}

int OSDir::getFileScanCount()
{
    if (info)
        return (int)info->nameList.size();
    else
        return -1;
}

bool OSDir::getNextFile(OSFile &oFile, const char* fileMask, bool bRecursive)
{
#ifdef _WIN32
    std::string realMask = "*.*";  //FIXME -- could this also be just '*' ?
#else
    std::string realMask = "*";
#endif
    if (fileMask)
        realMask = fileMask;

    realMask = TextUtils::toupper(realMask);

    if (info->namePos == -1)
    {
        info->nameList.clear();
        //FIXME -- just do the #ifdef'ing here?
        windowsAddFileStack(getFullOSPath(), realMask, bRecursive);
        linuxAddFileStack(getFullOSPath(), realMask, bRecursive);

        info->namePos = 0;
    }

    int size = info->nameList.size();
    if (info->namePos >= size)
    {
        info->namePos = -1;
        return false;
    }

    std::string fileName = info->nameList[info->namePos];

    if (osBaseDir.size()>1)
    {
        std::string temp = &(fileName.c_str()[osBaseDir.size()]);
        fileName = temp;
    }

    oFile.osName(fileName);
    info->namePos++;

    return true;
}

bool OSDir::getNextDir(OSDir &oDir, bool bRecursive)
{

#ifdef _WIN32
    std::string realMask = "*.*";  //FIXME -- could this also be just '*' ?
#else
    std::string realMask = "*";
#endif

    realMask = TextUtils::toupper(realMask);

    if (info->namePos == -1)
    {
        info->nameList.clear();
        //FIXME -- just do the #ifdef'ing here?
        windowsAddFileStack(getFullOSPath(), realMask, bRecursive, true);
        linuxAddFileStack(getFullOSPath(), realMask, bRecursive, true);

        info->namePos = 0;
    }

    int size = info->nameList.size();
    if (info->namePos >= size)
    {
        info->namePos = -1;
        return false;
    }

    std::string fileName = info->nameList[info->namePos];

    if (osBaseDir.size() > 1)
    {
        std::string temp = &(fileName.c_str()[osBaseDir.size()]);
        fileName = temp;
    }

    oDir.setOSDir(fileName);
    info->namePos++;

    return true;
}

bool OSDir::windowsAddFileStack(std::string pathName, std::string fileMask, bool bRecursive, bool bDirsOnly)
{
#ifdef _WIN32
    struct _finddata_t fileInfo;

    long    hFile;
    std::string searchstr;

    std::string FilePath;

    bool  bDone = false;

    searchstr = pathName;
    searchstr += "\\";
    if (fileMask.size() > 0)
        searchstr += fileMask;
    else
        searchstr += "*.*";

    hFile = (long)_findfirst(searchstr.c_str(), &fileInfo);

    if (hFile != -1)
    {
        while (!bDone)
        {
            if ((strlen(fileInfo.name) >0) && (strcmp(fileInfo.name,".") != 0) &&
                    (strcmp(fileInfo.name,"..") != 0))
            {
                FilePath = pathName;
                FilePath += "\\";
                FilePath += fileInfo.name;

                bool isSubDir = (fileInfo.attrib & _A_SUBDIR);

                if (bDirsOnly)
                {
                    if (isSubDir)
                    {
                        info->nameList.push_back(FilePath);
                        windowsAddFileStack(FilePath, fileMask, bRecursive, true);
                    }
                }
                else
                {
                    if (isSubDir && bRecursive)
                        windowsAddFileStack(FilePath, fileMask, bRecursive, false);
                    else if (!(fileInfo.attrib & _A_SUBDIR))
                        info->nameList.push_back(FilePath);
                }
            }
            if (_findnext(hFile,&fileInfo) == -1)
                bDone = true;
        }
    }
    return true;
#else
    // quell warnings
    if (!bRecursive || !bDirsOnly)
    {
        fileMask.size();
        pathName.size();
    }
    return false;
#endif
}

// linux mask filter functions
// we don't need these for windows as it can do it right in findNextFile
#ifndef _WIN32
static int match_multi(const char **mask, const char **string)
{
    const char *msk;
    const char *str;
    const char *msktop;
    const char *strtop;

    msk = *mask;
    str = *string;

    while ((*msk != '\0') && (*msk == '*'))
        msk++;    /* get rid of multiple '*'s */

    if (*msk == '\0') /* '*' was last, auto-match */
        return +1;

    msktop = msk;
    strtop = str;

    while (*msk != '\0')
    {
        if (*msk == '*')
        {
            *mask = msk;
            *string = str;
            return 0;     /* matched this segment */
        }
        else if (*str == '\0')
            return -1;    /* can't match */
        else
        {
            if ((*msk == '?') || (*msk == *str))
            {
                msk++;
                str++;
                if ((*msk == '\0') && (*str != '\0'))  /* advanced check */
                {
                    str++;
                    strtop++;
                    str = strtop;
                    msk = msktop;
                }
            }
            else
            {
                str++;
                strtop++;
                str = strtop;
                msk = msktop;
            }
        }
    }

    *mask = msk;
    *string = str;
    return +1;               /* full match */
}

static int match_mask (const char *mask, const char *string)
{
    if (mask == nullptr)
        return 0;

    if (string == nullptr)
        return 0;

    if ((mask[0] == '*') && (mask[1] == '\0'))
        return 1;         /* instant match */

    while (*mask != '\0')
    {
        if (*mask == '*')
        {
            mask++;
            switch (match_multi (&mask, &string))
            {
            case +1:
                return 1;
            case -1:
                return 0;
            }
        }
        else if (*string == '\0')
            return 0;
        else if ((*mask == '?') || (*mask == *string))
        {
            mask++;
            string++;
        }
        else
            return 0;
    }

    if (*string == '\0')
        return 1;
    else
        return 0;
}
#endif

bool OSDir::linuxAddFileStack(std::string pathName, std::string fileMask, bool bRecursive, bool bDirsOnly)
{
#ifdef _WIN32
    // quell warnings
    if (!bRecursive)
    {
        fileMask.size();
        pathName.size();
    }
    return false;
#else
    DIR  *directory;
    dirent  *fileInfo;
    struct stat  statbuf;
    char   searchstr[1024];
    std::string  FilePath;

    strncpy(searchstr, pathName.c_str(), sizeof(searchstr) - 1);
    searchstr[1023] = '\0';
    size_t pathLen = strlen(searchstr);
    if ((pathLen < 1022) && searchstr[pathLen - 1] != '/')
        searchstr[pathLen] = '/';
    directory = opendir(searchstr);
    if (!directory)
        return false;

    // TODO: make it use the filemask
    while ((fileInfo = readdir(directory)))
    {
        if (!((strcmp(fileInfo->d_name, ".") == 0) || (strcmp(fileInfo->d_name, "..") == 0)))
        {
            FilePath = searchstr;
            FilePath += fileInfo->d_name;
            strcpy(fileInfo->d_name, TextUtils::toupper(fileInfo->d_name).c_str());

            stat(FilePath.c_str(), &statbuf);

            bool isSubDir = S_ISDIR(statbuf.st_mode);

            if (bDirsOnly)
            {
                info->nameList.push_back(FilePath);
                if (bRecursive)
                    linuxAddFileStack(FilePath, fileMask, bRecursive);
            }
            else
            {
                if (isSubDir && bRecursive)
                    linuxAddFileStack(FilePath, fileMask, bRecursive);
                else if (match_mask(fileMask.c_str(), fileInfo->d_name))
                    info->nameList.push_back(FilePath);
            }
        }
    }
    closedir(directory);
    return true;
#endif// !Win32
}

// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
